Colors {
    -- Keenan palette
    Colors.black = rgba(0.0, 0.0, 0.0, 1.0)

    Colors.darkpurple = rgba(0.549,0.565,0.757, 1.0)
    Colors.purple2 = rgba(0.106, 0.122, 0.54, 0.2)
    Colors.lightpurple = rgba(0.816,0.824, 0.902, 1.0)

    Colors.midnightblue = rgba(0.14, 0.16, 0.52, 1.0)
    Colors.lightslategray = rgba(0.50, 0.51, 0.69, 1.0)
    Colors.silver = rgba(0.71, 0.72, 0.79, 1.0)
    Colors.gainsboro = rgba(0.87, 0.87, 0.87, 1.0)
    Colors.lightpink = rgba(0.90, 0.90, 1.0, 1.0)

    Colors.white = rgba(1.0, 1.0, 1.0, 1.0)
    Colors.red = rgba(1.0, 0.0, 0.0, 1.0)
    Colors.pink = rgba(1.0, 0.4, 0.7, 1.0)
    Colors.yellow = rgba(1.0, 1.0, 0.0, 1.0)
    Colors.orange = rgba(1.0, 0.6, 0.0, 1.0)
    Colors.green = rgba(0.0, 1.0, 0.0, 1.0)
    Colors.blue = rgba(0.0, 0.0, 1.0, 1.0)
    Colors.sky = rgba(0.325, 0.718, 0.769, 1.0)
    Colors.none = rgba(0.0, 0.0, 0.0, 0.0)
}

G {
  -- Drawing parameters
  G.padding = 60.0
  G.numArcPts = 30
  G.strokeWidth = 1.75
  G.spacing = 150.0
  G.pi = 3.14159
  G.two_pi = 6.28319
  G.toScreen = 200.0 -- Math space to screen space
  G.rayLen = 2.5
  G.arrowheadSize = 0.65
  G.textPadding = 7.0
  G.fontSize = "18pt"
  G.repelWeight = 0.0

  -- Poincare disk: Unit disk at the origin
  G.diskRadius = 1.0
  G.screenRadius = G.toScreen * G.diskRadius

  G.maxVal = 15.0 -- So the points don't look too much like ideal points
  G.maxValNorm = 5.0 -- So the points don't look too much like ideal points

  G.disk = Image {
	 x : 0.0
	 y : 0.0
	 w : G.screenRadius * 2.0
	 h : G.screenRadius * 2.0
	 rotation: 0.0
	 -- path : "HyperbolicBackground.svg"
	 path : "HyperbolicBackground2.svg"
  }

  -- TODO: looks like we can't parse an inline label??
  -- G.label = "$\mathbb{H}^2$"
  -- G.label = "\mathbb{H}^2"
  G.label = "H^2"
  G.labelPadding = 33.0 -- TODO: position by angle?
  G.textPosX = G.disk.x + G.screenRadius - G.labelPadding
  G.textPosY = G.disk.y + G.screenRadius - G.labelPadding

  G.text = Text {
  	 -- TODO: position in top right
  	 x : G.textPosX
	 y : G.textPosY
	 string : G.label
	 fontSize : G.fontSize
  }

  G.textLayering = G.text above G.disk
}

Point p {
      -- Point on hyperboloid of two sheets: x^2 + y^2 - z^2 = -1, where z > 0
      -- That is, <p, p>_L = 1 (with the Lorenz inner product)
      p.x = ?
      p.y = ?
      p.z = calcZ(p.x, p.y) -- Satisfies hyperbola by construction
      p.vec_math = [p.x, p.y, p.z]

      p.normFn = ensure lessThan(normSq(p.x, p.y), G.maxValNorm) -- Probably faster to inline all this

      p.vec_disk = toDisk(p.vec_math)
      p.vec_screen = diskToScreen(p.vec_disk, G.toScreen)

      p.shape = Circle {
	x : get(p.vec_screen, 0)
	y : get(p.vec_screen, 1)
	r : 3.0
	color : Colors.black
	strokeWidth : 1.0
	strokeColor : Colors.midnightblue
      }

      p.text = Text {
      	     x : ?
      	     y : ?
      	     string : p.label
	     fontSize : G.fontSize
      }

      p.labelFnMargin = ensure atDist(p.shape, p.text, G.textPadding)
      -- So if the label is big, the vertex doesn't end up INSIDE the label w/ some offset
      -- (Don't need it for small labels)
      -- p.labelFnOutside = encourage repel(p.shape, p.text)
      -- p.labelFnNear = encourage near(p.shape, p.text)

      p.layering = p.shape above G.disk
      p.textLayering = p.text above G.disk
}

-- Some test points on the hyperboloid
-- Point `p` {
--       override `p`.x = -10.0
--       override `p`.y = -10.0
--       -- override `p`.x = 0.0
--       -- override `p`.y = 0.0

--       override `p`.z = calcZ(`p`.x, `p`.y)
-- }

-- Point `q` {
--       override `q`.x = 4.0
--       override `q`.y = -3.0
--       override `q`.z = calcZ(`q`.x, `q`.y)
-- }

-- Point `r` {
--       override `r`.x = -1.0
--       override `r`.y = 2.0
--       override `r`.z = calcZ(`r`.xsh, `r`.y)
-- }

Segment e
where e := MkSegment(p, q)
with Point p; Point q {
     -- Line on hyperbola (geodesic)

     e.arcPath = slerpHyp(p.vec_math, q.vec_math, G.numArcPts)
     e.screenspacePath = pathToDiskAndScreen(e.arcPath, G.toScreen)

     -- Draw a path using the slerp'ed points
     e.shape = Curve {
     	     pathData : pathFromPoints(e.screenspacePath)
     	     -- polyline : e.screenspacePath
	     -- pathData : interpolate(e.shape.polyline)
	     strokeWidth : G.strokeWidth
	     fill : Colors.none
	     color : Colors.black
	     rotation : 0.0
     }

     e.layering1 = p.shape above e.shape
     e.layering2 = q.shape above e.shape
     e.layering3 = e.shape above G.disk

     -- If a point is in more than one segment, apply the objective to both
     -- LOCAL.labelAvoidFn_p = ensure labelDisjoint(e.shape, p.text, G.padding)
     -- LOCAL.labelAvoidFn_q = ensure labelDisjoint(e.shape, q.text, G.padding)

     LOCAL.labelAvoidFn_p = encourage repel(e.shape, p.text, G.repelWeight)
     LOCAL.labelAvoidFn_q = encourage repel(e.shape, q.text, G.repelWeight)
}

Ray r {
    r.length = 100.0
    -- r.shape = Line { 
    --    color : Colors.red
    -- } -- Otherwise can't write layering statement
    r.layeringDisk = r.shape above G.disk
}

-- Maybe it would make more sense to make the triangle from the segments that have already been made.

Triangle t
where t := MkTriangle(p, q, r)
with Point p; Point q; Point r {
     t.arcPath_pq = slerpHyp(p.vec_math, q.vec_math, G.numArcPts)
     t.screenspacePath_pq = pathToDiskAndScreen(t.arcPath_pq, G.toScreen)

     t.arcPath_qr = slerpHyp(q.vec_math, r.vec_math, G.numArcPts)
     t.screenspacePath_qr = pathToDiskAndScreen(t.arcPath_qr, G.toScreen)

     t.arcPath_rp = slerpHyp(r.vec_math, p.vec_math, G.numArcPts)
     t.screenspacePath_rp = pathToDiskAndScreen(t.arcPath_rp, G.toScreen)

     t.shape = Curve {
     	     pathData : join(t.screenspacePath_pq, t.screenspacePath_qr, t.screenspacePath_rp) -- Connect them in a particular order
	     strokeWidth : 0.0
	     fill : setOpacity(Colors.darkpurple, 0.3)
	     -- fill : setOpacity(Colors.purple2, 0.2)
	     color : Colors.none
	     rotation : 0.0
     }

     t.layering = t.shape above G.disk
}

-- TODO: Should this require a midpoint statement?
Ray r
with Segment s; Point p; Point q; Point m
where r := PerpendicularBisector(s); s := MkSegment(p, q); m := Midpoint(s) {
      r.rayDist = G.rayLen -- TODO: what's the right length to use here?
      r.tail_vec_math = halfwayPointHyp(p.vec_math, q.vec_math) -- The part w/o arrow, TODO
      r.head_vec_math = normalOnHyp(p.vec_math, q.vec_math, r.tail_vec_math, r.rayDist) -- TODO

      r.arcPath = slerpHyp(r.tail_vec_math, r.head_vec_math, G.numArcPts)
      r.screenspacePath = pathToDiskAndScreen(r.arcPath, G.toScreen)
      
      r.shape = Curve {
      	      pathData : pathFromPoints(r.screenspacePath) -- TODO: why are these NaNing?
	      strokeWidth : G.strokeWidth
	      fill : Colors.none
	      color : Colors.darkpurple
	      rotation : 0.0
	      rightArrowhead : True
	      arrowheadSize : G.arrowheadSize
      }

      -- Note this draws a mark in (flat) screen space because it's hard to draw a mark on the hyperboloid. 
      -- Squares don't look like squares in the Poincare disk.
      r.perpArcLen = 10.0 -- In screen space
      r.hypArcLen = G.pi / 29.0 -- In hyp space
      r.perpmark_screenspacePath = perpPathHyp(p.vec_math, q.vec_math, r.tail_vec_math, r.head_vec_math, r.perpArcLen, r.hypArcLen, G.toScreen)

      r.perpMark = Curve {
      	      pathData : polygonFromPoints(r.perpmark_screenspacePath) -- For the fill
	      strokeWidth : 1.25
	      color : Colors.black
	      fill : setOpacity(Colors.white, 0.5)
      }

      r.markLayering1 = r.perpMark below s.shape
      r.markLayering2 = r.perpMark below r.shape
      r.markLayering3 = r.perpMark above G.disk
      r.markLayering4 = r.perpMark below p.shape
      r.markLayering5 = r.perpMark below q.shape
      r.markLayering6 = r.perpMark below r.shape
      r.markLayering7 = r.perpMark below m.shape

     LOCAL.labelAvoidFn_Ray = encourage repel(r.perpMark, m.text, G.repelWeight)
     LOCAL.labelAvoidFn_Seg = encourage repel(s.shape, m.text, G.repelWeight)
}

Point p
where p := Midpoint(s); s := MkSegment(a, b)
with Segment s; Point a; Point b {
     override p.vec_math = halfwayPointHyp(a.vec_math, b.vec_math)

     override p.shape.color = Colors.white
     override p.shape.r = 3.2
     p.midLayering = p.shape above s.shape

     delete p.normFn
     LOCAL.labelAvoidFn_Midpt = ensure labelDisjoint(s.shape, p.text, G.padding)
}

Angle theta -- Angle QPR (i.e. the vertex point is p)
where theta := InteriorAngle(q, p, r)
with Point p; Point q; Point r {

     theta.radius = G.pi / 3.0 -- TODO: should this depend on distance from the center?
     theta.arcPath = arcPathHyp(p.vec_math, q.vec_math, r.vec_math, theta.radius)
     theta.arcPath_screenspace = pathToDiskAndScreen(theta.arcPath, G.toScreen)

     theta.shape = Curve {
     		 pathData : polygonFromPoints(theta.arcPath_screenspace)
		 strokeWidth : G.strokeWidth
		 color : Colors.darkpurple
		 fill : setOpacity(Colors.white, 0.5)
     }

     theta.layering1 = theta.shape above G.disk
     theta.layering2 = theta.shape below p.shape
     theta.layering3 = theta.shape below q.shape
     theta.layering4 = theta.shape below r.shape
}

Ray r
with Angle theta; Point x; Point y; Point z
where r := Bisector(theta); theta := InteriorAngle(y, x, z) { -- Angle YXZ (where X is the central point)

      -- TODO: note that there's a decent amount of code shared between here and the PerpendicularBisector selector
      -- Move some into the basic Ray selector?
      r.rayDist = G.rayLen -- TODO: what's the right length to use here?
      r.tail_vec_math = x.vec_math
      r.head_vec_math = angleBisectorHyp(x.vec_math, y.vec_math, z.vec_math, r.rayDist)

      r.arcPath = slerpHyp(r.tail_vec_math, r.head_vec_math, G.numArcPts)
      r.screenspacePath = pathToDiskAndScreen(r.arcPath, G.toScreen)
      
      r.shape = Curve {
      	      pathData : pathFromPoints(r.screenspacePath)
	      strokeWidth : G.strokeWidth
	      fill : Colors.none
	      color : Colors.darkpurple
	      rotation : 0.0
	      rightArrowhead : True
	      arrowheadSize : G.arrowheadSize
      }

      r.layeringArc = r.shape below theta.shape

      -- Bisect the arc twice more to get the bisector mark locations
      theta.bisectpt1 = angleBisectorHyp(x.vec_math, y.vec_math, r.head_vec_math, theta.radius)
      theta.vec_disk1 = toDisk(theta.bisectpt1)
      theta.vec_screen1 = diskToScreen(theta.vec_disk1, G.toScreen)

      theta.bisectpt2 = angleBisectorHyp(x.vec_math, z.vec_math, r.head_vec_math, theta.radius)
      theta.vec_disk2 = toDisk(theta.bisectpt2)
      theta.vec_screen2 = diskToScreen(theta.vec_disk2, G.toScreen)

      theta.markLen = 10.0

      -- Angle bisector marks: two tick marks
      -- Making this in screen space because we are drawing two straight (Euclidean) lines

      -- TODO: this should be two lines!
      theta.bisectMark1 = Curve {
      	      pathData : makeBisectorMark(theta.vec_screen1, x.vec_screen, theta.markLen)
      	      strokeWidth : G.strokeWidth
      	      fill : Colors.none
      	      color : Colors.darkpurple
      	      rotation : 0.0
      	      rightArrowhead : False
      	      arrowheadSize : G.arrowheadSize
      }

      theta.bisectMark2 = Curve {
      	      pathData : makeBisectorMark(theta.vec_screen2, x.vec_screen, theta.markLen)
      	      strokeWidth : G.strokeWidth
      	      fill : Colors.none
      	      color : Colors.darkpurple
      	      rotation : 0.0
      	      rightArrowhead : False
      	      arrowheadSize : G.arrowheadSize
      }

      theta.layeringMark1 = theta.bisectMark1 above theta.shape
      theta.layeringMark2 = theta.bisectMark2 above theta.shape
}

Triangle t; Angle theta {
	 LOCAL.layering = theta.shape above t.shape
}

Point p; Point q {
      -- LOCAL.shapeRepelFn = encourage repel(p.shape, q.shape)
      -- LOCAL.labelRepelFn = encourage repel(p.text, q.text)
}

Point p; Ray r {
      -- TODO: seems to make things really slow. Remove?
     -- LOCAL.labelAvoidFn_Ray = ensure labelDisjoint(r.shape, p.text, G.padding)

     LOCAL.labelAvoidFn_Ray = encourage repel(r.shape, p.text, G.repelWeight)
}
